package com.tiny.tinyweb.core.dao.entity;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ClassUtils;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tiny.tinyweb.core.annotation.TableInfo;
import com.tiny.tinyweb.core.utils.ConversionUtils;
import com.tiny.tinyweb.core.utils.JsonUtils;

/**
 * 所有实体类的基类，该类实现了{@link java.util.Map}接口。对于具体实体类中没有定义的字段，可使用
 * {@link java.util.Map#put(Object, Object)}和{@link java.util.Map#get(Object)}
 * 来访问。</BR> 定义具体实体类时要注意：</BR>
 * <ul>
 * <li>具体的实体类如果没有通过{@link TableInfo}注解配置表名，则将实体类名自动转换为表名。</li>
 * <li>具体的实体类如果没有通过{@link TableInfo}注解配置主键列表，则需要重写{@link #getPrimaryKeys}返回主键列表，
 * 否则认为没有定义主键。</li>
 * <li>{@link #getColumns}返回具体实体类中已定义属性的属性描述列表，但会忽略未定义字段。</li>
 * </ul>
 *
 * @author wxiaobin
 */
public abstract class MapEntity implements Map<String, Object>, Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 存储实体所对应的表的所有字段键值对的资源库。
     */
    private final Map<String, Object> repository = new HashMap<String, Object>();

    /**
     * 存储所有属性信息的资源库，以{@link java.beans.PropertyDescriptor}类型保存。
     */
    private final Map<String, PropertyDescriptor> columns = new HashMap<String, PropertyDescriptor>();

    /**
     * 获得该实体对应的表名，如果通过注解指定了表名，则返回注解的表名，否则返回根据驼峰式的实体类名转换为下划线分割的大写表名。
     *
     * @return 该实体对应的表名
     */
    public String getTableName() {
        TableInfo tableInfo = this.getClass().getAnnotation(TableInfo.class);
        if (tableInfo != null && !"".equals(tableInfo.name())) {
            return tableInfo.name();
        } else {
            return ConversionUtils.underscoreName(ClassUtils.getShortName(this.getClass()));
        }
    }

    /**
     * 获得该实体对应的表的主键列表。该表没有定义主键时，返回<code>null</code>。
     *
     * @return 该实体对应的表的主键
     */
    public List<String> getPrimaryKeys() {
        TableInfo tableInfo = this.getClass().getAnnotation(TableInfo.class);
        if (tableInfo != null && tableInfo.primaryKeys().length > 0) {
            return Arrays.asList(tableInfo.primaryKeys());
        } else {
            return null;
        }
    }

    /**
     * 获得该实体对应的表的乐观排它锁字段。该表没有定义乐观排他锁字段时，返回<code>null</code>。
     *
     * @return 该实体对应的表的乐观排它锁字段
     */
    public List<String> getLockKeys() {
        TableInfo tableInfo = this.getClass().getAnnotation(TableInfo.class);
        if (tableInfo != null && tableInfo.lockColumns().length > 0) {
            return Arrays.asList(tableInfo.lockColumns());
        } else {
            return null;
        }
    }

    /**
     * 获得该实体的所有属性的信息，每个属性信息都使用{@link org.springframework.beans.BeanUtils}包装成
     * {@link java.beans.PropertyDescriptor}之后被保存在{@link java.util.LinkedHashMap} 中，
     * 并使用已被转换为下划线分割的大写的表的字段名来访问。
     *
     * @return 该实体的所有属性的信息
     */
    public Map<String, PropertyDescriptor> getColumns() {
        // 直接返回已经获取到的字段信息
        if (!columns.isEmpty()) {
            return columns;
        }

        // 循环所有的属性，如果为可写属性，则获得其字段信息
        PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(this.getClass());
        for (PropertyDescriptor pd : pds) {
            if (pd.getWriteMethod() != null) {
                columns.put(ConversionUtils.underscoreName(pd.getName()), pd);
            }
        }
        return columns;
    }

    /**
     * 根据所有字段比较两个实体是否相等。只有当两个实体的所有字段都相等，才断定这两个实体的值是相等的，
     * 返回<code>true</code>否则返回<code>false</code>。如果指定了忽略空值，那么空值字段不会被作为比较对象。
     * 所有字段包含未定义的所有键值对。
     *
     * @param obj 比较对象
     * @param ignoreNull 是否忽略空值字段，等于<code>true</code>时，空值字段不会被作为比较对象
     * @return <code>true</code>：相等；<code>false</code>：不相等
     * @see {@link org.apache.commons.lang.builder.EqualsBuilder#append()},
     *      {@link org.apache.commons.lang.builder.EqualsBuilder#isEquals()}
     */
    public boolean equalsAll(Object obj, boolean ignoreNull) {
        // 快速检查
        if (obj == null) {
            // 非空对象必定不等于空对象
            return false;
        } else if (this == obj) {
            // 指向同一对象，必定相等
            return true;
        } else if (this.getClass() != obj.getClass()) {
            // 不同类的对象，必定不相等
            return false;
        } else {
            // 比较两个实体对象是否相等
            MapEntity entity = (MapEntity) obj;
            if (this.size() != entity.size()) {
                // 字段数目不一致，必定不相等
                return false;
            }
            // 构造一个 EqualsBuilder 对象来判断目标对象是否相等
            EqualsBuilder eb = new EqualsBuilder();
            for (String key : this.keySet()) {
                if (ignoreNull) {
                    if (this.get(key) != null) {
                        eb.append(this.get(key), entity.get(key));
                    }
                } else {
                    eb.append(this.get(key), entity.get(key));
                }
            }
            return eb.isEquals();
        }
    }

    /**
     * 比较两个实体是否相等。若果该实体对应的表有主键，则根据主键比较，否则根据所有字段比较。
     *
     * @param obj 比较对象
     * @return <code>true</code>：相等；<code>false</code>：不相等
     * @see {@link org.apache.commons.lang.builder.EqualsBuilder#append()},
     *      {@link org.apache.commons.lang.builder.EqualsBuilder#isEquals()},
     *      {@link #equalsAll(Object, boolean)}
     */
    @Override
    public boolean equals(Object obj) {
        // 快速检查
        if (obj == null) {
            // 非空对象必定不等于空对象
            return false;
        } else if (this == obj) {
            // 指向同一对象，必定相等
            return true;
        } else if (this.getClass() != obj.getClass()) {
            // 不同类的对象，必定不相等
            return false;
        } else {
            // 如果该实体对应的表没有主键，否则根据所有字段比较
            List<String> pks = this.getPrimaryKeys();
            if (pks == null || pks.isEmpty()) {
                return equalsAll(obj, false);
            }
            // 比较两个实体对象是否相等
            MapEntity entity = (MapEntity) obj;
            // 构造一个 EqualsBuilder 对象来判断目标对象是否相等
            EqualsBuilder eb = new EqualsBuilder();
            for (String key : this.keySet()) {
                if (pks.contains(key)) {
                    eb.append(this.get(key), entity.get(key));
                }
            }
            return eb.isEquals();
        }
    }

    /**
     * 获得该实体对象的散列码。若果该实体对应的表有主键，则根据主键散列，否则根据所有字段散列。
     * 所有字段包含未定义的所有键值对。
     *
     * @return 该实体对象的散列码
     * @see {@link org.apache.commons.lang.builder.HashCodeBuilder#append()},
     *      {@link org.apache.commons.lang.builder.HashCodeBuilder#hashCode()}
     */
    @Override
    public int hashCode() {
        List<String> pks = this.getPrimaryKeys();
        // 用实体对象的字段构造一个 HashCodeBuilder 来实现散列化
        HashCodeBuilder hcb = new HashCodeBuilder();
        // 如果该实体对应的表有主键，则根据主键散列，否则根据所有字段散列
        if (pks == null || pks.isEmpty()) {
            for (String key : this.keySet()) {
                hcb.append(this.get(key));
            }
        } else {
            for (String pk : pks) {
                hcb.append(this.get(pk));
            }
        }
        return hcb.toHashCode();
    }

    /**
     * 将该对象转换为字符串返回。
     *
     * @return 该对象的字符串表示
     * @see {@link org.apache.commons.lang.builder.ToStringBuilder#append()},
     *      {@link org.apache.commons.lang.builder.ToStringBuilder#toString()}
     */
    @Override
    public String toString() {
        ToStringBuilder tsb = new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE);
        for (String key : this.keySet()) {
            tsb.append(key, this.get(key));
        }
        return tsb.toString();
    }

    /**
     * 将该对象转换为JSON字符串返回。
     *
     * @return 该对象的JSON字符串
     * @throws Exception
     * @see {@link ObjectMapper#writeValueAsString(Object)}
     */
    public String toJSON() throws Exception {
        return JsonUtils.toJSON(this);
    }

    // <editor-fold defaultstate="collapsed" desc="interface methods of Map">
    @Override
    public void clear() {
        this.repository.clear();
    }

    @Override
    public boolean containsKey(Object key) {
        return this.repository.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
        return this.repository.containsValue(value);
    }

    @Override
    public Set<Entry<String, Object>> entrySet() {
        return this.repository.entrySet();
    }

    @Override
    public Object get(Object key) {
        return this.repository.get(key);
    }

    @Override
    public boolean isEmpty() {
        return this.repository.isEmpty();
    }

    @Override
    public Set<String> keySet() {
        return this.repository.keySet();
    }

    @Override
    public Object put(String key, Object value) {
        return this.repository.put(key, value);
    }

    @Override
    public void putAll(Map<? extends String, ? extends Object> m) {
        this.repository.putAll(m);
    }

    @Override
    public Object remove(Object key) {
        return this.repository.remove(key);
    }

    @Override
    public int size() {
        return this.repository.size();
    }

    @Override
    public Collection<Object> values() {
        return this.repository.values();
    }
    // </editor-fold>
}
